@yorz/cli 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/cli/index.js +815 -147
  2. package/dist/gui/assets/{abnfDiagram-VRR7QNED-Dysui3FU.js → abnfDiagram-VRR7QNED-Bfi5VmJM.js} +1 -1
  3. package/dist/gui/assets/{arc-Dva0_Fbz.js → arc-DW35HiMx.js} +1 -1
  4. package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-CSusN0wM.js → architectureDiagram-ZJ3FMSHR-D6ol2HjT.js} +1 -1
  5. package/dist/gui/assets/{blockDiagram-677ZJIJ3-Boy63uL4.js → blockDiagram-677ZJIJ3-DimsChzs.js} +1 -1
  6. package/dist/gui/assets/{c4Diagram-LMCZKHZV-DREOCesJ.js → c4Diagram-LMCZKHZV-aQtOPEhO.js} +1 -1
  7. package/dist/gui/assets/channel-CYD2fo3z.js +1 -0
  8. package/dist/gui/assets/{chunk-2Q5K7J3B-CQGkW5Le.js → chunk-2Q5K7J3B-Bg7XMADz.js} +1 -1
  9. package/dist/gui/assets/{chunk-32BRIVSS-ByKgaOB0.js → chunk-32BRIVSS-CJvj41Qg.js} +1 -1
  10. package/dist/gui/assets/{chunk-5VM5RSS4-ouTiIpFg.js → chunk-5VM5RSS4-B6iQkE6V.js} +1 -1
  11. package/dist/gui/assets/{chunk-EX3LRPZG-BBKKhIQU.js → chunk-EX3LRPZG-Bime3fML.js} +1 -1
  12. package/dist/gui/assets/{chunk-JWPE2WC7-i8qqnqZU.js → chunk-JWPE2WC7-B2d6pOPL.js} +1 -1
  13. package/dist/gui/assets/{chunk-MOJQB5TN-BmLvCJEW.js → chunk-MOJQB5TN-C2BtAAqD.js} +1 -1
  14. package/dist/gui/assets/{chunk-RYQCIY6F-DgIPwUtq.js → chunk-RYQCIY6F-DvOYjxsw.js} +1 -1
  15. package/dist/gui/assets/{chunk-V7JOEXUC-CHFlZVs7.js → chunk-V7JOEXUC-HRF7SSaJ.js} +1 -1
  16. package/dist/gui/assets/{chunk-VR4S4FIN-CF4gb-1V.js → chunk-VR4S4FIN-oMVpMTS0.js} +1 -1
  17. package/dist/gui/assets/{chunk-XXDRQBXY-6Ajr-TJy.js → chunk-XXDRQBXY-Gx6kiEHQ.js} +1 -1
  18. package/dist/gui/assets/classDiagram-OUVF2IWQ-CFzbN5IF.js +1 -0
  19. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-CFzbN5IF.js +1 -0
  20. package/dist/gui/assets/{cose-bilkent-JH36ORCC-Bq1NoHo9.js → cose-bilkent-JH36ORCC-MoHCpl37.js} +1 -1
  21. package/dist/gui/assets/{cynefin-VYW2F7L2-BLZ43yIC.js → cynefin-VYW2F7L2-2tnFSmNB.js} +1 -1
  22. package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-DOG8J1W-.js → cynefinDiagram-TSTJHNR4-Cp7uDUjl.js} +1 -1
  23. package/dist/gui/assets/{dagre-VKFMJZFB-6s-zIAym.js → dagre-VKFMJZFB-CTNuVp99.js} +1 -1
  24. package/dist/gui/assets/{diagram-FQU43EPY-MdHGL1Ep.js → diagram-FQU43EPY-Z9nTO-2q.js} +1 -1
  25. package/dist/gui/assets/{diagram-G47NLZAW-vNqROz2G.js → diagram-G47NLZAW-D1H3xA3x.js} +1 -1
  26. package/dist/gui/assets/{diagram-NH7WQ7WH-mBHJAt54.js → diagram-NH7WQ7WH-BGpM31nf.js} +1 -1
  27. package/dist/gui/assets/{diagram-OA4YK3LP-Duzu45NT.js → diagram-OA4YK3LP-Bhn6yNbV.js} +1 -1
  28. package/dist/gui/assets/{diagram-WEI45ONY-B9KIE7jm.js → diagram-WEI45ONY-BDJ6eaig.js} +1 -1
  29. package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-D-VxYOb1.js → ebnfDiagram-CCIWWBDH-C8UHOTF4.js} +1 -1
  30. package/dist/gui/assets/{erDiagram-Q63AITRT-bGX1YWnp.js → erDiagram-Q63AITRT-BpoLARsO.js} +1 -1
  31. package/dist/gui/assets/{flowDiagram-23GEKE2U-BFbLxzKU.js → flowDiagram-23GEKE2U-DSODAemd.js} +1 -1
  32. package/dist/gui/assets/{ganttDiagram-NO4QXBWP-CFlioSP9.js → ganttDiagram-NO4QXBWP-Bv5JA_dU.js} +1 -1
  33. package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-9m210Kgm.js → gitGraphDiagram-IHSO6WYX-CW7ObzYy.js} +1 -1
  34. package/dist/gui/assets/index-CAnHTFea.js +38 -0
  35. package/dist/gui/assets/index-Cft6LQWD.css +1 -0
  36. package/dist/gui/assets/{infoDiagram-FWYZ7A6U-D35ODxoC.js → infoDiagram-FWYZ7A6U-uTM_03X9.js} +1 -1
  37. package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-B-mZqJl4.js → ishikawaDiagram-FXEZZL3T-BlbCXOf8.js} +1 -1
  38. package/dist/gui/assets/{journeyDiagram-5HDEW3XC-CmFLCu-y.js → journeyDiagram-5HDEW3XC-BwtfR_HU.js} +1 -1
  39. package/dist/gui/assets/{kanban-definition-HUTT4EX6-C-risdsz.js → kanban-definition-HUTT4EX6-BXwvYv3u.js} +1 -1
  40. package/dist/gui/assets/{linear-D5TY5eoS.js → linear-C5c8PyN7.js} +1 -1
  41. package/dist/gui/assets/{mermaid.core-wGjSg1h-.js → mermaid.core-Dbw2b69i.js} +4 -4
  42. package/dist/gui/assets/{mindmap-definition-LN4V7U3C-BSdCxt4H.js → mindmap-definition-LN4V7U3C-9P8fXZBY.js} +1 -1
  43. package/dist/gui/assets/{pegDiagram-2B236MQR-DrexK9bW.js → pegDiagram-2B236MQR-81kUPbkr.js} +1 -1
  44. package/dist/gui/assets/{pieDiagram-ENE6RG2P-ay6UKmgt.js → pieDiagram-ENE6RG2P-BI7iwgtu.js} +1 -1
  45. package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-DgoOnLZO.js → quadrantDiagram-ABIIQ3AL-CSxA_Epi.js} +1 -1
  46. package/dist/gui/assets/{railroadDiagram-RFXS5EU6-Dsg3x2_p.js → railroadDiagram-RFXS5EU6-D4DnN2WS.js} +1 -1
  47. package/dist/gui/assets/{requirementDiagram-TGXJPOKE-BtbCKvwF.js → requirementDiagram-TGXJPOKE-DjNONyxQ.js} +1 -1
  48. package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-TabZvNT3.js → sankeyDiagram-HTMAVEWB-Di4S5cUR.js} +1 -1
  49. package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-C53WsGFo.js → sequenceDiagram-DBY2YBRQ-CLVwUzez.js} +1 -1
  50. package/dist/gui/assets/{sizeCapture-X5ZJPWSS-D_JRhNnb.js → sizeCapture-X5ZJPWSS-C5y0Ry_b.js} +1 -1
  51. package/dist/gui/assets/{stateDiagram-2N3HPSRC-D-_V4W7N.js → stateDiagram-2N3HPSRC-IdHsn6JH.js} +1 -1
  52. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CJBs-brH.js +1 -0
  53. package/dist/gui/assets/{swimlanes-5IMT3BWC-DgyOlILO.js → swimlanes-5IMT3BWC-bDCHEODt.js} +2 -2
  54. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BpY4RNvM.js +8 -0
  55. package/dist/gui/assets/{timeline-definition-FHXFAJF6-7M8Rj4NA.js → timeline-definition-FHXFAJF6-B6f6vq0E.js} +1 -1
  56. package/dist/gui/assets/{vennDiagram-L72KCM5P-DVyooCr2.js → vennDiagram-L72KCM5P-AjYH8jaU.js} +1 -1
  57. package/dist/gui/assets/{wardleyDiagram-EHGQE667-CYry07bZ.js → wardleyDiagram-EHGQE667-NV3g14vB.js} +1 -1
  58. package/dist/gui/assets/{xychartDiagram-FW5EYKEG-Dvx8ojch.js → xychartDiagram-FW5EYKEG-D4yDWaI8.js} +1 -1
  59. package/dist/gui/index.html +7 -12
  60. package/package.json +1 -1
  61. package/dist/gui/assets/channel-BAEHbRlk.js +0 -1
  62. package/dist/gui/assets/classDiagram-OUVF2IWQ-2TSTdFv6.js +0 -1
  63. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-2TSTdFv6.js +0 -1
  64. package/dist/gui/assets/index-DDGLj_Fo.js +0 -36
  65. package/dist/gui/assets/index-Dwx7eFhj.css +0 -1
  66. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-COKZr5A4.js +0 -1
  67. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BaU_Y0XV.js +0 -8
package/dist/cli/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir, platform } from "node:os";
3
3
  import { Command, Option } from "commander";
4
- import { stat, mkdir, readFile, writeFile, rename, rm, access, readdir, appendFile, copyFile } from "node:fs/promises";
5
- import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, sep, relative, normalize } from "node:path";
4
+ import { stat, mkdir, writeFile, rename, readFile, rm, access, readdir, appendFile, copyFile } from "node:fs/promises";
5
+ import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, relative, sep, normalize } from "node:path";
6
6
  import { createHash, randomUUID, randomBytes } from "node:crypto";
7
7
  import { existsSync, openSync, closeSync, statSync, readSync, mkdirSync, watch, createReadStream } from "node:fs";
8
8
  import { spawn, execFile, execFileSync } from "node:child_process";
@@ -23,7 +23,7 @@ import { createOpencode } from "@opencode-ai/sdk";
23
23
  import { createInterface as createInterface$1 } from "node:readline/promises";
24
24
  import MarkdownIt from "markdown-it";
25
25
  const name = "@yorz/cli";
26
- const version = "0.6.5";
26
+ const version = "0.7.1";
27
27
  const type = "module";
28
28
  const description = "";
29
29
  const bin = { "yorz": "./dist/cli/index.js" };
@@ -54,6 +54,93 @@ const pkg = {
54
54
  dependencies,
55
55
  devDependencies
56
56
  };
57
+ const HIDDEN_PROMPT_OPEN = "<!-- yorz:hidden -->";
58
+ const HIDDEN_PROMPT_CLOSE = "<!-- /yorz:hidden -->";
59
+ const SLASH_NAME_RE$1 = /^\/([\w-]+)(?:\s|$)/;
60
+ function matchCustomInstruction(prompt, instructions) {
61
+ const matched = SLASH_NAME_RE$1.exec(prompt.trim());
62
+ if (!matched) return null;
63
+ const name2 = matched[1];
64
+ return instructions.find((item) => item.name === name2) ?? null;
65
+ }
66
+ function wrapHiddenPrompt(hidden, original) {
67
+ const body = hidden.trim();
68
+ if (!body) return original;
69
+ return [HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE, original].join("\n");
70
+ }
71
+ function appendHiddenPrompt(original, hidden) {
72
+ const body = hidden.trim();
73
+ if (!body) return original;
74
+ return [original, "", HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE].join("\n");
75
+ }
76
+ function normalizeCustomInstructions(value) {
77
+ if (!Array.isArray(value)) return [];
78
+ const out = [];
79
+ const seen = /* @__PURE__ */ new Set();
80
+ for (const item of value) {
81
+ if (!item || typeof item !== "object") continue;
82
+ const obj = item;
83
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
84
+ const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
85
+ if (!id || !name2 || seen.has(id) || !/^[\w-]+$/.test(name2)) continue;
86
+ seen.add(id);
87
+ out.push({
88
+ id,
89
+ name: name2,
90
+ description: typeof obj.description === "string" ? obj.description : "",
91
+ // Fall back to the pre-rename `systemPrompt` so existing configs keep
92
+ // working; the next save rewrites them under the new key.
93
+ hiddenPrompt: typeof obj.hiddenPrompt === "string" ? obj.hiddenPrompt : typeof obj.systemPrompt === "string" ? obj.systemPrompt : "",
94
+ prefill: typeof obj.prefill === "string" ? obj.prefill : "",
95
+ createdAt: typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0
96
+ });
97
+ }
98
+ return out;
99
+ }
100
+ function parseCustomInstructions(value) {
101
+ if (value === void 0) return [];
102
+ if (!Array.isArray(value)) return { error: "customInstructions must be an array" };
103
+ const out = [];
104
+ const seen = /* @__PURE__ */ new Set();
105
+ for (const [index, item] of value.entries()) {
106
+ if (!item || typeof item !== "object") {
107
+ return { error: `customInstructions.${index} must be an object` };
108
+ }
109
+ const obj = item;
110
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
111
+ const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
112
+ if (!id) return { error: `customInstructions.${index}.id required` };
113
+ if (!name2 || !/^[\w-]+$/.test(name2)) {
114
+ return {
115
+ error: `customInstructions.${index}.name must use letters, numbers, underscores, or hyphens`
116
+ };
117
+ }
118
+ if (seen.has(id)) return { error: `duplicate custom instruction id: ${id}` };
119
+ seen.add(id);
120
+ const description2 = obj.description;
121
+ const hiddenPrompt = obj.hiddenPrompt ?? obj.systemPrompt;
122
+ const prefill = obj.prefill;
123
+ const createdAt = obj.createdAt;
124
+ if (typeof description2 !== "string") {
125
+ return { error: `customInstructions.${index}.description must be a string` };
126
+ }
127
+ if (typeof hiddenPrompt !== "string") {
128
+ return { error: `customInstructions.${index}.hiddenPrompt must be a string` };
129
+ }
130
+ if (typeof prefill !== "string") {
131
+ return { error: `customInstructions.${index}.prefill must be a string` };
132
+ }
133
+ if (typeof createdAt !== "number" || createdAt <= 0) {
134
+ return { error: `customInstructions.${index}.createdAt must be a positive number` };
135
+ }
136
+ out.push({ id, name: name2, description: description2, hiddenPrompt, prefill, createdAt });
137
+ }
138
+ return out;
139
+ }
140
+ function mergeCustomInstructions(project, global) {
141
+ const names = new Set(project.map((item) => item.name));
142
+ return [...project, ...global.filter((item) => !names.has(item.name))];
143
+ }
57
144
  const CURRENT_VERSION$1 = 1;
58
145
  const DEFAULT_GLOBAL_AGENT = { defaultKind: "claude" };
59
146
  const DEFAULT_NOTIFICATIONS = {
@@ -62,6 +149,11 @@ const DEFAULT_NOTIFICATIONS = {
62
149
  const DEFAULT_POWER = {
63
150
  inhibitWhenRunning: "system-default"
64
151
  };
152
+ const DEFAULT_APPEARANCE = {
153
+ themeMode: "system",
154
+ themeName: "terminal",
155
+ language: "zh-CN"
156
+ };
65
157
  const SHORTCUT_ACTION_IDS = [
66
158
  "newSpec",
67
159
  "toggleSpecDetailFullscreen",
@@ -74,6 +166,9 @@ function resolveGlobalConfigDir(env = process.env) {
74
166
  return join(base, "yorz");
75
167
  }
76
168
  function resolveGlobalConfigPath(env = process.env) {
169
+ return join(resolveGlobalConfigDir(env), "config.json");
170
+ }
171
+ function resolveLegacyGlobalProjectsPath(env = process.env) {
77
172
  return join(resolveGlobalConfigDir(env), "projects.json");
78
173
  }
79
174
  function resolveGlobalSkillsDir(env = process.env) {
@@ -84,7 +179,14 @@ function resolveSkillEntry(skillName, env = process.env) {
84
179
  }
85
180
  async function loadGlobalConfig(filePath) {
86
181
  const fp = filePath ?? resolveGlobalConfigPath();
87
- if (!existsSync(fp)) return defaultGlobalConfig();
182
+ if (!existsSync(fp)) {
183
+ const legacy = filePath ? basename$1(fp) === "config.json" ? join(dirname(fp), "projects.json") : "" : resolveLegacyGlobalProjectsPath();
184
+ if (legacy && existsSync(legacy)) return loadGlobalConfigFromPath(legacy);
185
+ return defaultGlobalConfig();
186
+ }
187
+ return loadGlobalConfigFromPath(fp);
188
+ }
189
+ async function loadGlobalConfigFromPath(fp) {
88
190
  let raw;
89
191
  try {
90
192
  raw = await readFile(fp, "utf8");
@@ -134,7 +236,9 @@ function normalizeConfig$1(value) {
134
236
  agent: normalizeAgent$1(obj.agent),
135
237
  notifications: normalizeNotifications(obj.notifications),
136
238
  shortcuts: normalizeShortcuts(obj.shortcuts),
137
- power: normalizePower(obj.power)
239
+ power: normalizePower(obj.power),
240
+ appearance: normalizeAppearance(obj.appearance),
241
+ customInstructions: normalizeCustomInstructions(obj.customInstructions)
138
242
  };
139
243
  }
140
244
  function defaultGlobalConfig() {
@@ -151,7 +255,9 @@ function defaultGlobalConfig() {
151
255
  shortcuts: {},
152
256
  power: {
153
257
  inhibitWhenRunning: DEFAULT_POWER.inhibitWhenRunning
154
- }
258
+ },
259
+ appearance: { ...DEFAULT_APPEARANCE },
260
+ customInstructions: []
155
261
  };
156
262
  }
157
263
  function normalizeAgent$1(value) {
@@ -202,6 +308,27 @@ function normalizePower(value) {
202
308
  }
203
309
  return { inhibitWhenRunning: DEFAULT_POWER.inhibitWhenRunning };
204
310
  }
311
+ function normalizeAppearance(value) {
312
+ if (!value || typeof value !== "object") return { ...DEFAULT_APPEARANCE };
313
+ const obj = value;
314
+ const themeMode = obj.themeMode;
315
+ const themeName = obj.themeName;
316
+ const language = obj.language;
317
+ return {
318
+ themeMode: isThemeMode(themeMode) ? themeMode : DEFAULT_APPEARANCE.themeMode,
319
+ themeName: isThemeName(themeName) ? themeName : DEFAULT_APPEARANCE.themeName,
320
+ language: isLanguage(language) ? language : DEFAULT_APPEARANCE.language
321
+ };
322
+ }
323
+ function isThemeMode(value) {
324
+ return value === "system" || value === "light" || value === "dark";
325
+ }
326
+ function isThemeName(value) {
327
+ return value === "terminal" || value === "graphite" || value === "paper";
328
+ }
329
+ function isLanguage(value) {
330
+ return value === "zh-CN" || value === "en";
331
+ }
205
332
  function normalizeShortcutBinding(value) {
206
333
  const parts = value.split("+").map((part) => part.trim()).filter(Boolean);
207
334
  let key = "";
@@ -1547,18 +1674,18 @@ function isYorzDebugCommand(prompt) {
1547
1674
  return /^\/yorz-debug(?:\s|$)/.test(prompt.trim());
1548
1675
  }
1549
1676
  function buildChatDebugPrompt(prompt, now = /* @__PURE__ */ new Date()) {
1550
- const body = prompt.trim().replace(/^\/yorz-debug(?:\s+|$)/, "").trim();
1677
+ const original = prompt.trim();
1678
+ const body = original.replace(/^\/yorz-debug(?:\s+|$)/, "").trim();
1551
1679
  const debugPath = `${CHAT_DEBUG_DIR_REL}/debug-${formatDebugTimestamp(now)}.md`;
1552
- const bug = body || "请先根据对话上下文确认待调试问题;若上下文不足,请向用户补齐复现信息。";
1553
- return [
1680
+ const guide = [
1554
1681
  `${skillRef("yorz-debug")},然后进入 Debug 模式。`,
1555
1682
  `本次是普通 chat 独立触发,没有 spec_dir。Debug 活文档必须写入临时文件 \`${debugPath}\`。`,
1556
1683
  `写入前请确保目录 \`${CHAT_DEBUG_DIR_REL}/\` 存在;该目录属于临时目录,会由 YorZ 定时清理。`,
1557
1684
  `如果该文件不存在则创建;本文件只承载本次 chat debug 记录,不需要追加复用其他文件。`,
1558
1685
  "",
1559
- "待调试问题:",
1560
- bug
1686
+ body ? "待调试问题见下方用户输入(忽略其中的 `/yorz-debug` 指令前缀):" : "请先根据对话上下文确认待调试问题;若上下文不足,请向用户补齐复现信息。"
1561
1687
  ].join("\n");
1688
+ return wrapHiddenPrompt(guide, original);
1562
1689
  }
1563
1690
  async function cleanupExpiredChatDebugFiles(projectPath, opts = {}) {
1564
1691
  const root = join(projectPath, CHAT_DEBUG_DIR_REL);
@@ -1607,6 +1734,169 @@ function formatDebugTimestamp(date) {
1607
1734
  pad(date.getSeconds())
1608
1735
  ].join("");
1609
1736
  }
1737
+ const CURRENT_VERSION = 1;
1738
+ const DEFAULT_SPECS_DIR = ".yorz/specs";
1739
+ function defaultProjectConfig() {
1740
+ return {
1741
+ version: CURRENT_VERSION,
1742
+ agent: { kind: "inherit" },
1743
+ specsDir: DEFAULT_SPECS_DIR,
1744
+ commands: [],
1745
+ customInstructions: []
1746
+ };
1747
+ }
1748
+ function configPath(cwd) {
1749
+ return join(cwd, ".yorz", "config.json");
1750
+ }
1751
+ async function loadProjectConfig(cwd) {
1752
+ const fp = configPath(cwd);
1753
+ if (!existsSync(fp)) return defaultProjectConfig();
1754
+ let raw;
1755
+ try {
1756
+ raw = await readFile(fp, "utf8");
1757
+ } catch {
1758
+ return defaultProjectConfig();
1759
+ }
1760
+ if (!raw.trim()) return defaultProjectConfig();
1761
+ let parsed;
1762
+ try {
1763
+ parsed = JSON.parse(raw);
1764
+ } catch {
1765
+ return defaultProjectConfig();
1766
+ }
1767
+ return normalizeConfig(parsed);
1768
+ }
1769
+ async function saveProjectConfig(cwd, cfg) {
1770
+ const fp = configPath(cwd);
1771
+ await mkdir(dirname(fp), { recursive: true });
1772
+ const normalized = normalizeConfig(cfg);
1773
+ const body = `${JSON.stringify(normalized, null, 2)}
1774
+ `;
1775
+ const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
1776
+ await writeFile(tmp, body, "utf8");
1777
+ await rename(tmp, fp);
1778
+ }
1779
+ function resolveSpecsDir(cwd, cfg) {
1780
+ const root = resolve(cwd);
1781
+ const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
1782
+ const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
1783
+ const rel = relative(root, abs);
1784
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
1785
+ throw new Error(`specsDir must stay within the project root: ${raw}`);
1786
+ }
1787
+ return abs;
1788
+ }
1789
+ async function ensureSpecsDirExists(absSpecsDir) {
1790
+ await mkdir(absSpecsDir, { recursive: true });
1791
+ }
1792
+ function normalizeConfig(value) {
1793
+ if (!value || typeof value !== "object") return defaultProjectConfig();
1794
+ const obj = value;
1795
+ const agent = normalizeAgent(obj.agent);
1796
+ const specsDir = normalizeSpecsDir(obj.specsDir);
1797
+ const commands = normalizeCommands(obj.commands);
1798
+ const customInstructions = normalizeCustomInstructions(obj.customInstructions);
1799
+ return { version: CURRENT_VERSION, agent, specsDir, commands, customInstructions };
1800
+ }
1801
+ function normalizeCommands(value) {
1802
+ if (!Array.isArray(value)) return [];
1803
+ const out = [];
1804
+ const seen = /* @__PURE__ */ new Set();
1805
+ for (const item of value) {
1806
+ if (!item || typeof item !== "object") continue;
1807
+ const obj = item;
1808
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
1809
+ const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
1810
+ const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
1811
+ if (!id || !name2 || !cli || seen.has(id)) continue;
1812
+ seen.add(id);
1813
+ const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
1814
+ out.push({ id, name: name2, cli, createdAt });
1815
+ }
1816
+ return out;
1817
+ }
1818
+ function normalizeAgent(value) {
1819
+ if (typeof value === "string") {
1820
+ if (value === "opencode") return { kind: "opencode" };
1821
+ if (value === "codex") return { kind: "codex" };
1822
+ return { kind: "claude" };
1823
+ }
1824
+ if (!value || typeof value !== "object") return { kind: "claude" };
1825
+ const obj = value;
1826
+ const kind = obj.kind;
1827
+ if (kind === "inherit") return { kind: "inherit" };
1828
+ if (kind === "opencode") return { kind: "opencode" };
1829
+ if (kind === "codex") return { kind: "codex" };
1830
+ if (kind === "custom") {
1831
+ const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
1832
+ if (!cmd) return { kind: "claude" };
1833
+ const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
1834
+ return { kind: "custom", cmd, args };
1835
+ }
1836
+ return { kind: "claude" };
1837
+ }
1838
+ function normalizeSpecsDir(value) {
1839
+ if (typeof value !== "string") return DEFAULT_SPECS_DIR;
1840
+ const trimmed = value.trim();
1841
+ if (!trimmed) return DEFAULT_SPECS_DIR;
1842
+ return trimmed;
1843
+ }
1844
+ const SLASH_NAME_RE = /^\/([\w-]+)(?:\s|$)/;
1845
+ const SPEC_COMMAND_RE = /^\/yorz-spec(?:\s|$)/;
1846
+ function isSlashCommand(prompt) {
1847
+ return SLASH_NAME_RE.test(prompt.trim());
1848
+ }
1849
+ function buildChatSpecPrompt(prompt, specsDirRelative) {
1850
+ const original = prompt.trim();
1851
+ const body = original.replace(SPEC_COMMAND_RE, "").trim();
1852
+ const guide = [
1853
+ `${skillRef("yorz-spec")},然后按其自动模式判定推进。`,
1854
+ `本次是普通 chat 独立触发,未指定 spec_path:若当前会话上下文中已出现过 spec 文档,继续推进该 spec;否则按 skill 的「新建 spec」流程创建后立即推进 plan 阶段。`,
1855
+ `spec 目录为 \`${specsDirRelative}/\`。`,
1856
+ "",
1857
+ body ? "本次需求见下方用户输入(忽略其中的 `/yorz-spec` 指令前缀):" : "请先根据对话上下文确认本次要处理的 spec 或需求;若上下文不足,请向用户补齐后再推进。"
1858
+ ].join("\n");
1859
+ return wrapHiddenPrompt(guide, original);
1860
+ }
1861
+ function buildUnknownCommandPrompt(prompt, hit) {
1862
+ const original = prompt.trim();
1863
+ const lines = [
1864
+ "下方用户输入以 `/` 开头,这是 YorZ 输入框的指令语法,**不是**你自身的 slash command,请勿按命令解析、也不要回复 `Unknown command`。"
1865
+ ];
1866
+ if (hit) {
1867
+ lines.push(
1868
+ `\`/${hit.name}\` 是用户在 YorZ 中配置的自定义指令,但未配置隐藏提示词${hit.description ? `;其说明为:${hit.description}` : ""}。请结合该指令名与下方正文理解用户意图。`
1869
+ );
1870
+ } else {
1871
+ lines.push("该指令未在 YorZ 中配置,请忽略指令前缀,按普通文本理解下方正文。");
1872
+ }
1873
+ return wrapHiddenPrompt(lines.join("\n"), original);
1874
+ }
1875
+ function resolveChatPrompt(prompt, instructions, opts = {}) {
1876
+ const original = prompt.trim();
1877
+ if (!isSlashCommand(original)) return { prompt, builtin: null };
1878
+ if (isYorzDebugCommand(original)) {
1879
+ return { prompt: buildChatDebugPrompt(original, opts.now), builtin: "yorz-debug" };
1880
+ }
1881
+ if (SPEC_COMMAND_RE.test(original)) {
1882
+ return {
1883
+ prompt: buildChatSpecPrompt(original, opts.specsDirRelative ?? ".yorz/specs"),
1884
+ builtin: "yorz-spec"
1885
+ };
1886
+ }
1887
+ const hit = matchCustomInstruction(original, instructions);
1888
+ if (hit?.hiddenPrompt.trim()) {
1889
+ return { prompt: wrapHiddenPrompt(hit.hiddenPrompt, original), builtin: null };
1890
+ }
1891
+ return { prompt: buildUnknownCommandPrompt(original, hit), builtin: null };
1892
+ }
1893
+ async function loadCustomInstructions(projectPath) {
1894
+ const [project, global] = await Promise.all([
1895
+ loadProjectConfig(projectPath).then((cfg) => cfg.customInstructions).catch(() => []),
1896
+ loadGlobalConfig().then((cfg) => cfg.customInstructions).catch(() => [])
1897
+ ]);
1898
+ return mergeCustomInstructions(project, global);
1899
+ }
1610
1900
  const KINDS = ["claude", "codex", "opencode"];
1611
1901
  const DRAFT_ID_RE = /^[a-zA-Z0-9-]{1,64}$/;
1612
1902
  function buildChatPrompt(prompt, draftId, attachments) {
@@ -1616,13 +1906,14 @@ function buildChatPrompt(prompt, draftId, attachments) {
1616
1906
  const rel = `${dir}/${a.storedName}`;
1617
1907
  return a.kind === "image" ? `- ![${a.name}](${rel})` : `- [${a.name}](${rel})`;
1618
1908
  });
1619
- return [
1909
+ return appendHiddenPrompt(
1620
1910
  prompt,
1621
- "",
1622
- "---",
1623
- `本次消息附带 ${attachments.length} 个附件,已保存在临时目录 \`${dir}/\`(请按需用文件工具直接读取,无需迁移):`,
1624
- ...lines
1625
- ].join("\n");
1911
+ [
1912
+ "---",
1913
+ `本次消息附带 ${attachments.length} 个附件,已保存在临时目录 \`${dir}/\`(请按需用文件工具直接读取,无需迁移):`,
1914
+ ...lines
1915
+ ].join("\n")
1916
+ );
1626
1917
  }
1627
1918
  function createSessionsRoutes(resolveProject) {
1628
1919
  const app = new Hono();
@@ -1638,6 +1929,11 @@ function createSessionsRoutes(resolveProject) {
1638
1929
  if (p instanceof Response) return p;
1639
1930
  return c.json(await p.sessions.listSessions());
1640
1931
  });
1932
+ app.get("/projects/:projectId/agent-usage", async (c) => {
1933
+ const p = await need(c);
1934
+ if (p instanceof Response) return p;
1935
+ return c.json(await p.sessions.getUsageStatus());
1936
+ });
1641
1937
  app.post("/projects/:projectId/sessions", async (c) => {
1642
1938
  const p = await need(c);
1643
1939
  if (p instanceof Response) return p;
@@ -1689,7 +1985,11 @@ function createSessionsRoutes(resolveProject) {
1689
1985
  }
1690
1986
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
1691
1987
  if (!prompt) return c.json({ error: "prompt required" }, 400);
1692
- let finalPrompt = isYorzDebugCommand(prompt) ? buildChatDebugPrompt(prompt) : prompt;
1988
+ const instructions = isSlashCommand(prompt) ? await loadCustomInstructions(p.path) : [];
1989
+ const resolved = resolveChatPrompt(prompt, instructions, {
1990
+ specsDirRelative: p.specsDirRelative
1991
+ });
1992
+ let finalPrompt = resolved.prompt;
1693
1993
  if (body.draftId !== void 0) {
1694
1994
  if (typeof body.draftId !== "string" || !DRAFT_ID_RE.test(body.draftId)) {
1695
1995
  return c.json({ error: "draftId has invalid format" }, 400);
@@ -1700,9 +2000,9 @@ function createSessionsRoutes(resolveProject) {
1700
2000
  finalPrompt = buildChatPrompt(finalPrompt, draftId, metas);
1701
2001
  }
1702
2002
  }
1703
- if (isYorzDebugCommand(prompt)) void cleanupExpiredChatDebugFiles(p.path).catch(() => {
2003
+ if (resolved.builtin === "yorz-debug") void cleanupExpiredChatDebugFiles(p.path).catch(() => {
1704
2004
  });
1705
- const handle = await p.sessions.send(c.req.param("sid"), finalPrompt);
2005
+ const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt);
1706
2006
  return c.json({ runId: handle.runId, sessionId: handle.sessionId }, 202);
1707
2007
  });
1708
2008
  app.post("/projects/:projectId/sessions/:sid/abort", async (c) => {
@@ -1713,13 +2013,13 @@ function createSessionsRoutes(resolveProject) {
1713
2013
  });
1714
2014
  return app;
1715
2015
  }
1716
- const execFileP$1 = promisify(execFile);
2016
+ const execFileP$2 = promisify(execFile);
1717
2017
  function withHiddenWindowsConsole(options, platform2 = process.platform) {
1718
2018
  if (platform2 !== "win32") return options;
1719
2019
  return { ...options, windowsHide: true };
1720
2020
  }
1721
2021
  async function execFileWithoutWindow(file, args, options) {
1722
- const result = await execFileP$1(file, args, withHiddenWindowsConsole(options));
2022
+ const result = await execFileP$2(file, args, withHiddenWindowsConsole(options));
1723
2023
  return { stdout: result.stdout, stderr: result.stderr };
1724
2024
  }
1725
2025
  function spawnWithoutWindow(command, args, options) {
@@ -1876,7 +2176,7 @@ async function commit(cwd, opts) {
1876
2176
  const extraPaths = renamed.map((r) => r.renamedFrom);
1877
2177
  const allPaths = [...opts.paths, ...extraPaths];
1878
2178
  await runGit(cwd, ["add", "--", ...allPaths]);
1879
- await runGit(cwd, ["commit", "-m", message, "--", ...allPaths]);
2179
+ await runGit(cwd, ["commit", "-m", message]);
1880
2180
  const { stdout } = await runGit(cwd, ["rev-parse", "HEAD"]);
1881
2181
  return { commit: stdout.trim() };
1882
2182
  }
@@ -2451,111 +2751,6 @@ function createProjectRoutes(registry, worktreeManager) {
2451
2751
  });
2452
2752
  return app;
2453
2753
  }
2454
- const CURRENT_VERSION = 1;
2455
- const DEFAULT_SPECS_DIR = ".yorz/specs";
2456
- function defaultProjectConfig() {
2457
- return {
2458
- version: CURRENT_VERSION,
2459
- agent: { kind: "inherit" },
2460
- specsDir: DEFAULT_SPECS_DIR,
2461
- commands: []
2462
- };
2463
- }
2464
- function configPath(cwd) {
2465
- return join(cwd, ".yorz", "config.json");
2466
- }
2467
- async function loadProjectConfig(cwd) {
2468
- const fp = configPath(cwd);
2469
- if (!existsSync(fp)) return defaultProjectConfig();
2470
- let raw;
2471
- try {
2472
- raw = await readFile(fp, "utf8");
2473
- } catch {
2474
- return defaultProjectConfig();
2475
- }
2476
- if (!raw.trim()) return defaultProjectConfig();
2477
- let parsed;
2478
- try {
2479
- parsed = JSON.parse(raw);
2480
- } catch {
2481
- return defaultProjectConfig();
2482
- }
2483
- return normalizeConfig(parsed);
2484
- }
2485
- async function saveProjectConfig(cwd, cfg) {
2486
- const fp = configPath(cwd);
2487
- await mkdir(dirname(fp), { recursive: true });
2488
- const normalized = normalizeConfig(cfg);
2489
- const body = `${JSON.stringify(normalized, null, 2)}
2490
- `;
2491
- const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
2492
- await writeFile(tmp, body, "utf8");
2493
- await rename(tmp, fp);
2494
- }
2495
- function resolveSpecsDir(cwd, cfg) {
2496
- const root = resolve(cwd);
2497
- const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
2498
- const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
2499
- const rel = relative(root, abs);
2500
- if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
2501
- throw new Error(`specsDir must stay within the project root: ${raw}`);
2502
- }
2503
- return abs;
2504
- }
2505
- async function ensureSpecsDirExists(absSpecsDir) {
2506
- await mkdir(absSpecsDir, { recursive: true });
2507
- }
2508
- function normalizeConfig(value) {
2509
- if (!value || typeof value !== "object") return defaultProjectConfig();
2510
- const obj = value;
2511
- const agent = normalizeAgent(obj.agent);
2512
- const specsDir = normalizeSpecsDir(obj.specsDir);
2513
- const commands = normalizeCommands(obj.commands);
2514
- return { version: CURRENT_VERSION, agent, specsDir, commands };
2515
- }
2516
- function normalizeCommands(value) {
2517
- if (!Array.isArray(value)) return [];
2518
- const out = [];
2519
- const seen = /* @__PURE__ */ new Set();
2520
- for (const item of value) {
2521
- if (!item || typeof item !== "object") continue;
2522
- const obj = item;
2523
- const id = typeof obj.id === "string" ? obj.id.trim() : "";
2524
- const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
2525
- const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
2526
- if (!id || !name2 || !cli || seen.has(id)) continue;
2527
- seen.add(id);
2528
- const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
2529
- out.push({ id, name: name2, cli, createdAt });
2530
- }
2531
- return out;
2532
- }
2533
- function normalizeAgent(value) {
2534
- if (typeof value === "string") {
2535
- if (value === "opencode") return { kind: "opencode" };
2536
- if (value === "codex") return { kind: "codex" };
2537
- return { kind: "claude" };
2538
- }
2539
- if (!value || typeof value !== "object") return { kind: "claude" };
2540
- const obj = value;
2541
- const kind = obj.kind;
2542
- if (kind === "inherit") return { kind: "inherit" };
2543
- if (kind === "opencode") return { kind: "opencode" };
2544
- if (kind === "codex") return { kind: "codex" };
2545
- if (kind === "custom") {
2546
- const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
2547
- if (!cmd) return { kind: "claude" };
2548
- const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
2549
- return { kind: "custom", cmd, args };
2550
- }
2551
- return { kind: "claude" };
2552
- }
2553
- function normalizeSpecsDir(value) {
2554
- if (typeof value !== "string") return DEFAULT_SPECS_DIR;
2555
- const trimmed = value.trim();
2556
- if (!trimmed) return DEFAULT_SPECS_DIR;
2557
- return trimmed;
2558
- }
2559
2754
  function createProjectConfigRoutes(registry) {
2560
2755
  const app = new Hono();
2561
2756
  app.get("/projects/:projectId/config", async (c) => {
@@ -2582,7 +2777,8 @@ function createProjectConfigRoutes(registry) {
2582
2777
  version: 1,
2583
2778
  agent: parsed.agent,
2584
2779
  specsDir: parsed.specsDir,
2585
- commands: current.commands
2780
+ commands: current.commands,
2781
+ customInstructions: current.customInstructions
2586
2782
  };
2587
2783
  let absSpecsDir;
2588
2784
  try {
@@ -2603,6 +2799,34 @@ function createProjectConfigRoutes(registry) {
2603
2799
  await registry.reload(id);
2604
2800
  return c.json({ ok: true, config: cfg });
2605
2801
  });
2802
+ app.get("/projects/:projectId/custom-instructions", async (c) => {
2803
+ const id = c.req.param("projectId");
2804
+ const entry = await registry.findEntry(id);
2805
+ if (!entry) return c.json({ error: "project not found" }, 404);
2806
+ const cfg = await loadProjectConfig(entry.path);
2807
+ return c.json({ customInstructions: cfg.customInstructions });
2808
+ });
2809
+ app.put("/projects/:projectId/custom-instructions", async (c) => {
2810
+ const id = c.req.param("projectId");
2811
+ const entry = await registry.findEntry(id);
2812
+ if (!entry) return c.json({ error: "project not found" }, 404);
2813
+ let raw;
2814
+ try {
2815
+ raw = await c.req.json();
2816
+ } catch {
2817
+ return c.json({ error: "invalid JSON body" }, 400);
2818
+ }
2819
+ if (!raw || typeof raw !== "object") return c.json({ error: "body must be an object" }, 400);
2820
+ const parsed = parseCustomInstructions(raw.customInstructions);
2821
+ if ("error" in parsed) return c.json({ error: parsed.error }, 400);
2822
+ const current = await loadProjectConfig(entry.path);
2823
+ try {
2824
+ await saveProjectConfig(entry.path, { ...current, customInstructions: parsed });
2825
+ } catch (err) {
2826
+ return c.json({ error: err.message }, 500);
2827
+ }
2828
+ return c.json({ ok: true, customInstructions: parsed });
2829
+ });
2606
2830
  return app;
2607
2831
  }
2608
2832
  function parseBody$1(value) {
@@ -2802,7 +3026,9 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
2802
3026
  agent: cfg.agent,
2803
3027
  notifications: cfg.notifications,
2804
3028
  shortcuts: cfg.shortcuts,
2805
- power: cfg.power
3029
+ power: cfg.power,
3030
+ appearance: cfg.appearance,
3031
+ customInstructions: cfg.customInstructions
2806
3032
  });
2807
3033
  });
2808
3034
  app.put("/global-config", async (c) => {
@@ -2819,6 +3045,8 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
2819
3045
  cfg.notifications = parsed.notifications;
2820
3046
  cfg.shortcuts = parsed.shortcuts;
2821
3047
  cfg.power = parsed.power;
3048
+ cfg.appearance = parsed.appearance;
3049
+ cfg.customInstructions = parsed.customInstructions;
2822
3050
  await saveGlobalConfig(cfg, globalConfigPath);
2823
3051
  await powerController.refresh();
2824
3052
  return c.json({
@@ -2827,7 +3055,9 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
2827
3055
  agent: cfg.agent,
2828
3056
  notifications: cfg.notifications,
2829
3057
  shortcuts: cfg.shortcuts,
2830
- power: cfg.power
3058
+ power: cfg.power,
3059
+ appearance: cfg.appearance,
3060
+ customInstructions: cfg.customInstructions
2831
3061
  }
2832
3062
  });
2833
3063
  });
@@ -2887,13 +3117,33 @@ function parseBody(value) {
2887
3117
  error: "power.inhibitWhenRunning must be system-default | prevent-display-sleep | keep-system-awake"
2888
3118
  };
2889
3119
  }
3120
+ const appearance = parseAppearance(obj.appearance ?? DEFAULT_APPEARANCE);
3121
+ if ("error" in appearance) return appearance;
3122
+ const customInstructions = parseCustomInstructions(obj.customInstructions);
3123
+ if ("error" in customInstructions) return customInstructions;
2890
3124
  return {
2891
3125
  agent: { defaultKind },
2892
3126
  notifications: { sessionEnd: { banner: s.banner, sound: s.sound } },
2893
3127
  shortcuts: normalizeShortcuts(shortcutObj),
2894
- power: { inhibitWhenRunning }
3128
+ power: { inhibitWhenRunning },
3129
+ appearance,
3130
+ customInstructions
2895
3131
  };
2896
3132
  }
3133
+ function parseAppearance(value) {
3134
+ if (!value || typeof value !== "object") return { error: "appearance must be an object" };
3135
+ const obj = value;
3136
+ const themeMode = obj.themeMode;
3137
+ if (!isThemeMode(themeMode))
3138
+ return { error: "appearance.themeMode must be system | light | dark" };
3139
+ const themeName = obj.themeName;
3140
+ if (!isThemeName(themeName)) {
3141
+ return { error: "appearance.themeName must be terminal | graphite | paper" };
3142
+ }
3143
+ const language = obj.language;
3144
+ if (!isLanguage(language)) return { error: "appearance.language must be zh-CN | en" };
3145
+ return { themeMode, themeName, language };
3146
+ }
2897
3147
  function createSpecDraftsRoutes(resolveProject) {
2898
3148
  const app = new Hono();
2899
3149
  const need = async (c) => {
@@ -4104,7 +4354,7 @@ class RegistryEventBus {
4104
4354
  this.scheduleEmit();
4105
4355
  });
4106
4356
  } catch (err) {
4107
- registryLog().warn("projects.json watch failed", { dir, message: err.message });
4357
+ registryLog().warn("global config watch failed", { dir, message: err.message });
4108
4358
  }
4109
4359
  }
4110
4360
  stop() {
@@ -5305,6 +5555,37 @@ function toolResultText(content) {
5305
5555
  }
5306
5556
  return content == null ? "" : JSON.stringify(content);
5307
5557
  }
5558
+ const USAGE_WINDOWS = [
5559
+ ["five_hour", "5-hour"],
5560
+ ["seven_day", "7-day"],
5561
+ ["seven_day_sonnet", "Sonnet 7-day"],
5562
+ ["seven_day_opus", "Opus 7-day"],
5563
+ ["seven_day_oauth_apps", "OAuth apps 7-day"]
5564
+ ];
5565
+ function usageWindows(usage) {
5566
+ const out = [];
5567
+ const limits = usage.rate_limits;
5568
+ if (!limits) return out;
5569
+ for (const [key, label] of USAGE_WINDOWS) {
5570
+ const win = limits[key];
5571
+ if (!win) continue;
5572
+ out.push({
5573
+ key,
5574
+ label,
5575
+ utilization: typeof win.utilization === "number" ? win.utilization : null,
5576
+ resetsAt: typeof win.resets_at === "string" ? win.resets_at : null
5577
+ });
5578
+ }
5579
+ for (const win of limits.model_scoped ?? []) {
5580
+ out.push({
5581
+ key: `model:${win.display_name}`,
5582
+ label: win.display_name,
5583
+ utilization: typeof win.utilization === "number" ? win.utilization : null,
5584
+ resetsAt: typeof win.resets_at === "string" ? win.resets_at : null
5585
+ });
5586
+ }
5587
+ return out;
5588
+ }
5308
5589
  class ClaudeSession {
5309
5590
  constructor(id, cwd, isNew) {
5310
5591
  this.id = id;
@@ -5414,10 +5695,40 @@ class ClaudeAdapter {
5414
5695
  return out;
5415
5696
  }
5416
5697
  capabilities() {
5417
- return { listSessions: true, getMessages: true };
5698
+ return { listSessions: true, getMessages: true, usageStatus: true };
5699
+ }
5700
+ async getUsageStatus() {
5701
+ const options = {
5702
+ cwd: this.cwd,
5703
+ permissionMode: "bypassPermissions",
5704
+ allowDangerouslySkipPermissions: true
5705
+ };
5706
+ const q = query({ prompt: "", options });
5707
+ try {
5708
+ const usage = await q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET();
5709
+ q.close();
5710
+ return {
5711
+ kind: this.kind,
5712
+ status: usage.rate_limits_available ? "available" : "unavailable",
5713
+ checkedAt: Date.now(),
5714
+ subscriptionType: usage.subscription_type,
5715
+ rateLimitsAvailable: usage.rate_limits_available,
5716
+ windows: usageWindows(usage),
5717
+ message: usage.rate_limits_available ? void 0 : "claude usage rate limits are unavailable for this auth/provider"
5718
+ };
5719
+ } catch (err) {
5720
+ q.close();
5721
+ return {
5722
+ kind: this.kind,
5723
+ status: "error",
5724
+ checkedAt: Date.now(),
5725
+ message: err instanceof Error ? err.message : String(err)
5726
+ };
5727
+ }
5418
5728
  }
5419
5729
  }
5420
5730
  const DEFAULT_CODEX_STORAGE_ROOT = join(homedir(), ".codex");
5731
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
5421
5732
  const TITLE_MAX_LENGTH$1 = 64;
5422
5733
  function codexThreadOptions(cwd) {
5423
5734
  return {
@@ -5474,6 +5785,107 @@ function parseCodexSessionIndex(text) {
5474
5785
  }
5475
5786
  return out;
5476
5787
  }
5788
+ function asRecord$1(value) {
5789
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5790
+ }
5791
+ function numberFrom$1(value) {
5792
+ if (typeof value === "number" && Number.isFinite(value)) return value;
5793
+ if (typeof value === "string" && value.trim()) {
5794
+ const n = Number(value);
5795
+ if (Number.isFinite(n)) return n;
5796
+ }
5797
+ return null;
5798
+ }
5799
+ function resetFrom$1(value) {
5800
+ if (typeof value === "string" && value.trim()) {
5801
+ const n2 = Number(value);
5802
+ if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
5803
+ const d = new Date(value);
5804
+ return Number.isFinite(d.getTime()) ? d.toISOString() : null;
5805
+ }
5806
+ const n = numberFrom$1(value);
5807
+ return n == null ? null : new Date(n * 1e3).toISOString();
5808
+ }
5809
+ function codexWindow(key, label, value) {
5810
+ const rec = asRecord$1(value);
5811
+ if (!rec) return null;
5812
+ const utilization = numberFrom$1(rec.used_percent) ?? numberFrom$1(rec.usedPercent) ?? numberFrom$1(rec.utilization) ?? numberFrom$1(rec.percent_used);
5813
+ if (utilization == null) return null;
5814
+ return {
5815
+ key,
5816
+ label,
5817
+ utilization,
5818
+ resetsAt: resetFrom$1(rec.resets_at ?? rec.reset_at ?? rec.resetsAt ?? rec.resetAt)
5819
+ };
5820
+ }
5821
+ function parseCodexUsageResponse(raw) {
5822
+ const root = asRecord$1(raw);
5823
+ if (!root) return null;
5824
+ const rateLimit = asRecord$1(root.rate_limit);
5825
+ const rateLimits = asRecord$1(root.rate_limits);
5826
+ const windows = [];
5827
+ const primary = codexWindow(
5828
+ "primary",
5829
+ "Primary",
5830
+ rateLimit?.primary_window ?? rateLimits?.primary ?? root.primary
5831
+ );
5832
+ if (primary) windows.push(primary);
5833
+ const secondary = codexWindow(
5834
+ "secondary",
5835
+ "Secondary",
5836
+ rateLimit?.secondary_window ?? rateLimits?.secondary ?? root.secondary
5837
+ );
5838
+ if (secondary) windows.push(secondary);
5839
+ const additional = root.additional_rate_limits ?? root.additionalRateLimits ?? rateLimit?.additional;
5840
+ if (Array.isArray(additional)) {
5841
+ for (const [idx, item] of additional.entries()) {
5842
+ const rec = asRecord$1(item);
5843
+ const label = String(rec?.label ?? rec?.name ?? rec?.limit_name ?? `Additional ${idx + 1}`);
5844
+ const win = codexWindow(`additional:${idx}`, label, item);
5845
+ if (win) windows.push(win);
5846
+ }
5847
+ }
5848
+ if (windows.length === 0) return null;
5849
+ return {
5850
+ kind: "codex",
5851
+ status: "available",
5852
+ checkedAt: Date.now(),
5853
+ source: "private-api",
5854
+ subscriptionType: typeof root.plan_type === "string" ? root.plan_type : null,
5855
+ rateLimitsAvailable: true,
5856
+ windows
5857
+ };
5858
+ }
5859
+ function parseCodexTokenCountSnapshot(text) {
5860
+ let latestTs = 0;
5861
+ let latest = null;
5862
+ for (const line of text.split("\n")) {
5863
+ const trimmed = line.trim();
5864
+ if (!trimmed) continue;
5865
+ let obj;
5866
+ try {
5867
+ obj = JSON.parse(trimmed);
5868
+ } catch {
5869
+ continue;
5870
+ }
5871
+ const rec = asRecord$1(obj);
5872
+ const payload = asRecord$1(rec?.payload);
5873
+ if (payload?.type !== "token_count") continue;
5874
+ const parsed = parseCodexUsageResponse({ rate_limits: payload.rate_limits });
5875
+ if (!parsed) continue;
5876
+ const ts = typeof rec?.timestamp === "string" ? Date.parse(rec.timestamp) : 0;
5877
+ if (ts >= latestTs) {
5878
+ latestTs = ts;
5879
+ latest = {
5880
+ ...parsed,
5881
+ checkedAt: Number.isFinite(ts) && ts > 0 ? ts : Date.now(),
5882
+ source: "local-snapshot",
5883
+ message: "codex local token_count snapshot"
5884
+ };
5885
+ }
5886
+ }
5887
+ return latest;
5888
+ }
5477
5889
  class CodexSession {
5478
5890
  constructor(thread) {
5479
5891
  this.thread = thread;
@@ -5597,7 +6009,52 @@ class CodexAdapter {
5597
6009
  return out;
5598
6010
  }
5599
6011
  capabilities() {
5600
- return { listSessions: true, getMessages: true };
6012
+ return { listSessions: true, getMessages: true, usageStatus: true };
6013
+ }
6014
+ async getUsageStatus() {
6015
+ const live = await this.readPrivateUsage().catch(() => null);
6016
+ if (live) return live;
6017
+ const snapshot = await this.readLatestUsageSnapshot().catch(() => null);
6018
+ if (snapshot) return snapshot;
6019
+ return {
6020
+ kind: this.kind,
6021
+ status: "unavailable",
6022
+ checkedAt: Date.now(),
6023
+ message: "codex usage is unavailable: private API failed and no local token_count snapshot found"
6024
+ };
6025
+ }
6026
+ async readPrivateUsage() {
6027
+ const auth = await this.readAuth();
6028
+ if (!auth) return null;
6029
+ const res = await fetch(CODEX_USAGE_URL, {
6030
+ headers: {
6031
+ authorization: `Bearer ${auth.accessToken}`,
6032
+ "chatgpt-account-id": auth.accountId
6033
+ }
6034
+ });
6035
+ if (!res.ok) return null;
6036
+ return parseCodexUsageResponse(await res.json());
6037
+ }
6038
+ async readAuth() {
6039
+ let raw;
6040
+ try {
6041
+ raw = JSON.parse(await readFile(join(this.storageRoot, "auth.json"), "utf8"));
6042
+ } catch {
6043
+ return null;
6044
+ }
6045
+ const tokens = asRecord$1(asRecord$1(raw)?.tokens);
6046
+ const accessToken = typeof tokens?.access_token === "string" ? tokens.access_token : "";
6047
+ const accountId = typeof tokens?.account_id === "string" ? tokens.account_id : "";
6048
+ return accessToken && accountId ? { accessToken, accountId } : null;
6049
+ }
6050
+ async readLatestUsageSnapshot() {
6051
+ let latest = null;
6052
+ for (const file of await this.walkRollouts()) {
6053
+ const parsed = parseCodexTokenCountSnapshot(await readFile(file, "utf8"));
6054
+ if (!parsed) continue;
6055
+ if (!latest || parsed.checkedAt >= latest.checkedAt) latest = parsed;
6056
+ }
6057
+ return latest;
5601
6058
  }
5602
6059
  async walkRollouts() {
5603
6060
  const out = [];
@@ -5684,6 +6141,137 @@ class CodexAdapter {
5684
6141
  return null;
5685
6142
  }
5686
6143
  }
6144
+ const execFileP$1 = promisify(execFile);
6145
+ const OPENCODE_QUOTA_INSTALL = "npx @slkiser/opencode-quota@latest init";
6146
+ const OPENCODE_QUOTA_PLUGIN_BIN = join(
6147
+ homedir(),
6148
+ ".cache",
6149
+ "opencode",
6150
+ "packages",
6151
+ "@slkiser",
6152
+ "opencode-quota@latest",
6153
+ "node_modules",
6154
+ "@slkiser",
6155
+ "opencode-quota",
6156
+ "dist",
6157
+ "bin",
6158
+ "opencode-quota.js"
6159
+ );
6160
+ function asRecord(value) {
6161
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
6162
+ }
6163
+ function numberFrom(value) {
6164
+ if (typeof value === "number" && Number.isFinite(value)) return value;
6165
+ if (typeof value === "string" && value.trim()) {
6166
+ const n = Number(value);
6167
+ if (Number.isFinite(n)) return n;
6168
+ }
6169
+ return null;
6170
+ }
6171
+ function resetFrom(value) {
6172
+ if (typeof value === "string" && value.trim()) {
6173
+ const n2 = Number(value);
6174
+ if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
6175
+ const d = new Date(value);
6176
+ return Number.isFinite(d.getTime()) ? d.toISOString() : null;
6177
+ }
6178
+ const n = numberFrom(value);
6179
+ return n == null ? null : new Date(n * 1e3).toISOString();
6180
+ }
6181
+ function usageWindowFromRecord(rec, idx) {
6182
+ const label = String(rec.label ?? rec.name ?? rec.title ?? rec.provider ?? `Window ${idx + 1}`);
6183
+ const used = numberFrom(rec.usedPercent) ?? numberFrom(rec.used_percent) ?? numberFrom(rec.percent) ?? numberFrom(rec.utilization);
6184
+ const remaining = numberFrom(rec.remainingPercent) ?? numberFrom(rec.remaining_percent) ?? numberFrom(rec.percentRemaining);
6185
+ const utilization = used ?? (remaining == null ? null : 100 - remaining);
6186
+ if (utilization == null) return null;
6187
+ return {
6188
+ key: String(rec.key ?? rec.id ?? label),
6189
+ label,
6190
+ utilization,
6191
+ resetsAt: resetFrom(rec.resetsAt ?? rec.resets_at ?? rec.resetAt ?? rec.reset_at ?? rec.reset)
6192
+ };
6193
+ }
6194
+ function collectOpencodeQuotaWindows(value, out, seen) {
6195
+ if (!value || seen.has(value)) return;
6196
+ seen.add(value);
6197
+ if (Array.isArray(value)) {
6198
+ for (const item of value) collectOpencodeQuotaWindows(item, out, seen);
6199
+ return;
6200
+ }
6201
+ const rec = asRecord(value);
6202
+ if (!rec) return;
6203
+ const win = usageWindowFromRecord(rec, out.length);
6204
+ if (win) out.push(win);
6205
+ for (const key of ["windows", "quotas", "items", "limits", "entries"]) {
6206
+ if (key in rec) collectOpencodeQuotaWindows(rec[key], out, seen);
6207
+ }
6208
+ for (const key of ["providers", "data"]) {
6209
+ const child = asRecord(rec[key]);
6210
+ if (child) {
6211
+ for (const value2 of Object.values(child)) collectOpencodeQuotaWindows(value2, out, seen);
6212
+ } else if (key in rec) {
6213
+ collectOpencodeQuotaWindows(rec[key], out, seen);
6214
+ }
6215
+ }
6216
+ }
6217
+ function parseOpencodeQuotaOutput(text) {
6218
+ let raw;
6219
+ try {
6220
+ raw = JSON.parse(text);
6221
+ } catch {
6222
+ return parseOpencodeQuotaText(text);
6223
+ }
6224
+ const windows = [];
6225
+ collectOpencodeQuotaWindows(raw, windows, /* @__PURE__ */ new Set());
6226
+ if (windows.length === 0) return null;
6227
+ return {
6228
+ kind: "opencode",
6229
+ status: "available",
6230
+ checkedAt: Date.now(),
6231
+ source: "external-cli",
6232
+ rateLimitsAvailable: true,
6233
+ windows
6234
+ };
6235
+ }
6236
+ function parseOpencodeQuotaText(text) {
6237
+ const windows = [];
6238
+ let provider = "";
6239
+ for (const line of text.split("\n")) {
6240
+ const providerMatch = line.match(/^\s*→\s*\[(.+?)]/);
6241
+ if (providerMatch) {
6242
+ provider = providerMatch[1].trim();
6243
+ continue;
6244
+ }
6245
+ const quotaMatch = line.match(/^\s*(.+?quota)\s+.*?(\d+(?:\.\d+)?)%\s+left/i);
6246
+ if (!quotaMatch) continue;
6247
+ const label = [provider, quotaMatch[1].trim()].filter(Boolean).join(": ");
6248
+ const remaining = Number(quotaMatch[2]);
6249
+ windows.push({
6250
+ key: label || `quota:${windows.length}`,
6251
+ label: label || `Quota ${windows.length + 1}`,
6252
+ utilization: Math.max(0, Math.min(100, 100 - remaining)),
6253
+ resetsAt: null
6254
+ });
6255
+ }
6256
+ if (windows.length === 0) return null;
6257
+ return {
6258
+ kind: "opencode",
6259
+ status: "available",
6260
+ checkedAt: Date.now(),
6261
+ source: "external-cli",
6262
+ rateLimitsAvailable: true,
6263
+ windows
6264
+ };
6265
+ }
6266
+ async function opencodeQuotaCommands() {
6267
+ const commands = [{ cmd: "opencode-quota", args: ["show", "--json"] }];
6268
+ try {
6269
+ await access(OPENCODE_QUOTA_PLUGIN_BIN);
6270
+ commands.push({ cmd: process.execPath, args: [OPENCODE_QUOTA_PLUGIN_BIN, "show", "--json"] });
6271
+ } catch {
6272
+ }
6273
+ return commands;
6274
+ }
5687
6275
  function textParts(parts) {
5688
6276
  const out = [];
5689
6277
  for (const p of parts) {
@@ -5713,7 +6301,9 @@ class OpenCodeSession {
5713
6301
  if (this.aborted) return;
5714
6302
  const data = res.data;
5715
6303
  if (!data) {
5716
- const errMsg = res.error && typeof res.error === "object" && "data" in res.error ? String(res.error.data?.message ?? "prompt failed") : "prompt failed";
6304
+ const errMsg = res.error && typeof res.error === "object" && "data" in res.error ? String(
6305
+ res.error.data?.message ?? "prompt failed"
6306
+ ) : "prompt failed";
5717
6307
  yield { type: "error", message: errMsg };
5718
6308
  return;
5719
6309
  }
@@ -5795,7 +6385,51 @@ class OpenCodeAdapter {
5795
6385
  return out;
5796
6386
  }
5797
6387
  capabilities() {
5798
- return { listSessions: true, getMessages: true };
6388
+ return { listSessions: true, getMessages: true, usageStatus: true };
6389
+ }
6390
+ async getUsageStatus() {
6391
+ let lastError;
6392
+ for (const command of await opencodeQuotaCommands()) {
6393
+ try {
6394
+ const { stdout } = await execFileP$1(command.cmd, command.args, {
6395
+ cwd: this.cwd,
6396
+ timeout: 1e4,
6397
+ maxBuffer: 1024 * 1024
6398
+ });
6399
+ const parsed = parseOpencodeQuotaOutput(stdout);
6400
+ if (parsed) return parsed;
6401
+ return {
6402
+ kind: this.kind,
6403
+ status: "error",
6404
+ checkedAt: Date.now(),
6405
+ source: "external-cli",
6406
+ message: "opencode-quota returned no parseable quota windows"
6407
+ };
6408
+ } catch (err2) {
6409
+ lastError = err2;
6410
+ if (err2.code === "ENOENT") continue;
6411
+ break;
6412
+ }
6413
+ }
6414
+ const err = lastError;
6415
+ const code = err?.code;
6416
+ if (code === "ENOENT") {
6417
+ return {
6418
+ kind: this.kind,
6419
+ status: "unavailable",
6420
+ checkedAt: Date.now(),
6421
+ source: "external-cli",
6422
+ installCommand: OPENCODE_QUOTA_INSTALL,
6423
+ message: "opencode-quota is not installed"
6424
+ };
6425
+ }
6426
+ return {
6427
+ kind: this.kind,
6428
+ status: "error",
6429
+ checkedAt: Date.now(),
6430
+ source: "external-cli",
6431
+ message: err instanceof Error ? err.message : String(err)
6432
+ };
5799
6433
  }
5800
6434
  async dispose() {
5801
6435
  if (!this.booting) return;
@@ -5937,14 +6571,17 @@ class SessionManager {
5937
6571
  const indexed2 = byId.get(info.id);
5938
6572
  if (!indexed2) {
5939
6573
  byId.set(info.id, info);
5940
- } else if (isOpaqueTitle(indexed2.title, indexed2.id) && !isOpaqueTitle(info.title, info.id)) {
5941
- byId.set(info.id, {
5942
- ...indexed2,
5943
- title: info.title,
5944
- createdAt: Math.min(indexed2.createdAt, info.createdAt),
5945
- updatedAt: Math.max(indexed2.updatedAt, info.updatedAt)
5946
- });
6574
+ continue;
6575
+ }
6576
+ const merged = {
6577
+ ...indexed2,
6578
+ createdAt: Math.min(indexed2.createdAt, info.createdAt),
6579
+ updatedAt: Math.max(indexed2.updatedAt, info.updatedAt)
6580
+ };
6581
+ if (isOpaqueTitle(indexed2.title, indexed2.id) && !isOpaqueTitle(info.title, info.id)) {
6582
+ merged.title = info.title;
5947
6583
  }
6584
+ byId.set(info.id, merged);
5948
6585
  }
5949
6586
  } catch {
5950
6587
  }
@@ -5952,8 +6589,11 @@ class SessionManager {
5952
6589
  const alive = [...byId.values()].filter(
5953
6590
  (s) => nativeIds.has(s.id) || s.createdAt !== s.updatedAt || this.running.has(s.id)
5954
6591
  );
6592
+ const activityRank = (s) => this.running.has(s.id) ? 1 : 0;
5955
6593
  const out = [];
5956
- for (const s of alive.sort((a, b) => b.updatedAt - a.updatedAt)) {
6594
+ for (const s of alive.sort(
6595
+ (a, b) => activityRank(b) - activityRank(a) || b.updatedAt - a.updatedAt
6596
+ )) {
5957
6597
  out.push({ ...s, running: this.running.has(s.id) });
5958
6598
  if (out.length >= SESSION_LIST_LIMIT) break;
5959
6599
  }
@@ -6001,6 +6641,27 @@ class SessionManager {
6001
6641
  if (!adapter.capabilities().getMessages) return [];
6002
6642
  return adapter.getMessages(sid);
6003
6643
  }
6644
+ async getUsageStatus() {
6645
+ const adapter = this.adapters.get(this.defaultKind);
6646
+ if (!adapter.capabilities().usageStatus || !adapter.getUsageStatus) {
6647
+ return {
6648
+ kind: this.defaultKind,
6649
+ status: "unavailable",
6650
+ checkedAt: Date.now(),
6651
+ message: `${this.defaultKind} adapter does not support usage status queries`
6652
+ };
6653
+ }
6654
+ try {
6655
+ return await adapter.getUsageStatus();
6656
+ } catch (err) {
6657
+ return {
6658
+ kind: this.defaultKind,
6659
+ status: "error",
6660
+ checkedAt: Date.now(),
6661
+ message: err instanceof Error ? err.message : String(err)
6662
+ };
6663
+ }
6664
+ }
6004
6665
  async ensureLive(sid) {
6005
6666
  const existing = this.live.get(sid);
6006
6667
  if (existing) return existing;
@@ -6012,12 +6673,19 @@ class SessionManager {
6012
6673
  if (this.disposing) session.abort();
6013
6674
  return ls;
6014
6675
  }
6015
- async send(sid, prompt) {
6676
+ /**
6677
+ * `titleSource` lets callers derive the session title from the user's own
6678
+ * text while sending an expanded prompt to the Agent. Without it, an injected
6679
+ * hidden prompt would become the title.
6680
+ */
6681
+ async send(sid, prompt, titleSource) {
6016
6682
  const runId = randomUUID();
6017
6683
  const emitter = this.emitterFor(sid);
6018
6684
  let currentSid = sid;
6019
6685
  const startedAt = Date.now();
6020
- await this.maybeUpdateTitleFromPrompt(currentSid, prompt).catch(() => {
6686
+ await this.maybeUpdateTitleFromPrompt(currentSid, titleSource ?? prompt).catch(() => {
6687
+ });
6688
+ await this.store.touch(sid).catch(() => {
6021
6689
  });
6022
6690
  this.setRunning(sid, true);
6023
6691
  agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });